package tms.model;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.w3c.dom.Element;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * TaskTable class is a singleton representing the tasks in the TMS
 * It extends AbstractModelObject and implements PropertyChangeListener
 * to allow the fire of property changes
 * 
 * @author Amer Alhalabi/Rolf Schmidt
 * @version 2.0
 * 
 */
public class TaskTable extends AbstractModelObject implements
		PropertyChangeListener {

	/**
	 * header string for the csv export of the tasks
	 */
	private static final String HEADER_STRING = "ID,Title,Description,Duration,Deliverable,Deadline,Person\n";

	/**
	 * the only instance of the class
	 */
	private static TaskTable s_instance = null;

	/**
	 * holds all Task objects (taskId, Task)
	 */
	TreeMap<String, Task> taskList = new TreeMap<String, Task>();

	/**
	 * getter of the only instance of the class
	 * 
	 * @return the only instance of the class
	 */
	public static TaskTable getInstance() {
		if (s_instance == null)
			s_instance = new TaskTable();
		return s_instance;
	}

	/**
	 * private constructor to prevent the creation the multiple instances of the class
	 */
	private TaskTable() {
	};

	/**
	 * a getter of tasks as a map
	 * 
	 * @return a map of task ID to its Task object
	 */
	public TreeMap<String, Task> getTaskMap() {
		return taskList;
	}

	/**
	 * a getter of tasks as a list
	 * 
	 * @return a list of tasks
	 */
	public List<Task> getTaskList() {
		List<Task> taskL = new ArrayList<Task>();
		for (Map.Entry<String, Task> entry : taskList.entrySet())
			taskL.add(entry.getValue());
		return taskL;
	}

	/**
	 * Add a task to the task table.
	 * If the task to be added is null or already exists, then it will not be added
	 * A tasks change property event will be fired if the addition of the task is successful
	 * 
	 * @param task
	 *            the task to be added
	 */
	public void addTask(Task task) {
		if (task == null)
			return;
		if (taskList.containsKey(Integer.toString(task.getId())))
			return;
		TreeMap<String, Task> oldValue = taskList;
		taskList = new TreeMap<String, Task>(taskList);
		taskList.put(Integer.toString(task.getId()), task);
		firePropertyChange("tasks", oldValue, taskList);
	}

	/**
	 * remove a task from the task table
	 * If the task to be removed is null or does not exist, then it will not be removed
	 * A tasks change property event will be fired if the removal of the task is successful
	 * 
	 * @param task
	 *            the task to be removed
	 */
	public void removeTask(Task task) {
		if (task == null)
			return;
		if (!taskList.containsKey(Integer.toString(task.getId())))
			return;
		TreeMap<String, Task> oldValue = taskList;
		taskList = new TreeMap<String, Task>(taskList);
		taskList.remove(Integer.toString(task.getId()));
		firePropertyChange("tasks", oldValue, taskList);
	}

	/**
	 * getter of the personList as an Arraylist of Person objects
	 * 
	 * @return the personList
	 */
	public ArrayList<Person> getPersonlist() {
		TreeMap<String, Person> personList = new TreeMap<String, Person>();
		ArrayList<Person> people = new ArrayList<Person>();

		// loop ever the the tasks in the taskList, and for each task, get the person name
		// if the person name is already in the person arraylist, then add the task to that person taskList.
		// if the person name does not exist in the person arrylist, create a new person object 
		// and add the task to its taskList.
		for (Task task : this.getTaskList()) {
			String name = task.getPerson();
			if (!personList.containsKey(name)) {
				Person person = new Person();
				person.setName(name);
				personList.put(name, person);
			}
			Person person = personList.get(name);
			person.addTask(task);
		}
		for (Map.Entry<String, Person> entry : personList.entrySet()) {
			people.add(entry.getValue());
		}
		return people;
	}

	/**
	 * get the value from an XML tag as string
	 * 
	 * @param tag
	 *            the tag to get
	 * @param element
	 *            the element to get the tag from
	 * @return the string representation of the node value
	 */
	private static String getTagValue(String tag, Element element) {
		NodeList nodeList = element.getElementsByTagName(tag).item(0)
				.getChildNodes();
		Node node = (Node) nodeList.item(0);

		return node.getNodeValue();
	}

	/**
	 * helper function to process the XML input file
	 * 
	 * @param subTasks
	 *            the subtasks to go process
	 * @return true if the subtasks were processed successfully, false otherwise
	 */
	private boolean processSubTasks(NodeList subTasks) {

		for (int i = 0; i < subTasks.getLength(); i++) {
			Node myNode = subTasks.item(i);

			if (myNode.getNodeName().equals("subtasks")) {
				processSubTasks(myNode.getChildNodes());
			}

			if (myNode.getNodeName().equals("subtask")) {
				Element eElement = (Element) myNode;

				Task task = new Task();
				task.setId(Integer.parseInt(getTagValue("id", eElement)));
				task.setTitle(getTagValue("title", eElement));
				task.setDescription(getTagValue("description", eElement));

				String taskId = Integer.toString(task.getId());

				if (taskList.containsKey(taskId)) {
					taskList.clear();
					System.out.println("invalid file content - task:" + taskId
							+ "already exists");
					System.out.println("operation aborted");
					return false;
				}

				task.addPropertyChangeListener(this);
				addTask(task);
			}
		}

		return true;
	}

	/**
	 * openFile populates the tasktable instance with tasks that are read from
	 * an input file.
	 * If the filename is invalid (empty string, null, or does not exist) or if the format and content
	 * of the file is invalid, no tasks will be added to the task table and false will be return.
	 *  
	 * 
	 * @param filename
	 *            the path to the input file that contains the tasks to be
	 *            populated in the system
	 */
	public boolean openXMLFile(String fileName) {

		TreeMap<String, Person> personList = PeopleTable.getInstance()
				.getPersonMap();
		taskList.clear();
		try {
			File fXmlFile = new File(fileName);
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory
					.newInstance();
			DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
			Document doc = dBuilder.parse(fXmlFile);
			doc.getDocumentElement().normalize();

			// read a task
			NodeList nList = doc.getElementsByTagName("task");

			// for each element in the task, get the task attributes and validate them
			// if the validation is successful, add the task to the tasktable
			// and add a property change listener to the task; the listener is the tasktable
			// and add the person assigned to the task to the peopletable
			for (int i = 0; i < nList.getLength(); i++) {

				Node nNode = nList.item(i);

				if (nNode.getNodeType() == Node.ELEMENT_NODE) {

					Element eElement = (Element) nNode;

					Calendar cal = Calendar.getInstance();
					SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
					try {
						cal.setTime(sdf
								.parse(getTagValue("deadline", eElement)));
					} catch (ParseException e) {
						System.out.println("Deadline ("
								+ getTagValue("deadline", eElement)
								+ ") could not be converted.");
						throw e;
					}

					Task task = new Task();
					task.setId(Integer.parseInt(getTagValue("id", eElement)));
					task.setTitle(getTagValue("title", eElement));
					task.setDescription(getTagValue("description", eElement));
					task.setDuration(Integer.parseInt(getTagValue("duration",
							eElement)));
					task.setDeliverable(getTagValue("deliverable", eElement));
					task.setDeadline(cal.getTime());
					task.setPerson(getTagValue("person", eElement));

					String name = task.getPerson();
					String taskId = Integer.toString(task.getId());

					if (taskList.containsKey(taskId)) {
						taskList.clear();
						personList.clear();
						System.out.println("invalid file content - task:"
								+ taskId + "already exists");
						System.out.println("operation aborted");
						return false;
					}

					task.addPropertyChangeListener(this);
					this.addTask(task);

					if (!personList.containsKey(name)) {
						Person person = new Person();
						person.setName(name);
						personList.put(name, person);
					}
					Person person = personList.get(name);
					person.addTask(task);

					NodeList subTasks = eElement
							.getElementsByTagName("subtasks");

					if (!processSubTasks(subTasks)) {
						return false;
					}
				}
			}
		} catch (Exception e) {
			System.out.println("Exception in openXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		}
		PeopleTable.getInstance().setPersonList(this.getPersonlist());
		firePropertyChange("taskList", null, taskList);
		return true;
	}

	/**
	 * save the people table to a CSV file
	 * 
	 * @param fileName
	 *            path to the file where the content of the task tree will be
	 *            saved
	 * @return true if the file was saved successfully, false otherwise
	 */
	public Boolean saveCSVFile(String fileName) {

		// do not allow the filename to be empty or null
		if (fileName == null || fileName.equals("")) {
			System.out.println("Filename is invalid (null or empty)");
			return false;
		}
		PrintWriter printOut = null;
		// add the header to the stringbuiler that will be written to the CSV file
		StringBuilder sb = new StringBuilder(HEADER_STRING);

		try {
			printOut = new PrintWriter(fileName);

			// loop over the task objects and retrieve its attribute
			// separate each attribute with a comma
			// and add that to a stringbuilder, which will written to the CSV file
			ArrayList<Task> taskList = (ArrayList<Task>) getTaskList();
			for (Task task : taskList) {
				sb.append(task.getId());
				sb.append(",");
				sb.append(task.getTitle());
				sb.append(",");
				sb.append(task.getDescription());
				sb.append(",");
				sb.append(task.getDuration());
				sb.append(",");
				sb.append(task.getDeliverable());
				sb.append(",");
				sb.append(task.getDeadline());
				sb.append(",");
				sb.append(task.getPerson());
				sb.append("\n");
			}
			printOut.print(sb);
		} catch (FileNotFoundException e) {
			System.out.println("File: " + fileName);
			System.out.println(e.getMessage());
			return false;
		} finally {
			printOut.close();
		}
		return true;
	}

	@Override
	/**
	 * this method gets called whenever there is a property change
	 * 
	 * @param evt a PropertyChangeEvent object describing the event source and the property that has changed.
	 */
	public void propertyChange(PropertyChangeEvent evt) {
		PeopleTable.getInstance().setPersonList(this.getPersonlist());
	}
}